package hu.monitor.remoting.client;

import hu.monitor.remoting.InvocationContext;
import hu.monitor.remoting.InvocationException;
import hu.monitor.remoting.InvokerInterceptor;
import hu.monitor.remoting.transport.Client;
import hu.monitor.remoting.transport.ClientPool;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;


public class RemoteCallInterceptor implements InvokerInterceptor {
	private final ClientPool clientPool;

	public RemoteCallInterceptor(final ClientPool endpoint) {
		this.clientPool = endpoint;
	}

	@Override
	public Object intercept(final InvocationContext invocationContext) throws Throwable {
		final MarshalledInvocation invocation = this.createMarshalledInvocation(invocationContext);
		final Client client = this.clientPool.getClient();
		Object ret;
		try {
			client.connect();
			ret = client.send(invocation);
		} catch (final IOException ex) {
			throw new InvocationException(ex);
		} finally {
			this.clientPool.releaseClient(client);
		}

		if (ret instanceof Throwable) {
			this.throwException(invocationContext.getMethod(), (Throwable)ret);
		}
		return ret;

	}

	private void throwException(final Method method, final Throwable ex) throws Throwable {
		final Set<Class<?>> exceptions = this.resolveThrowableExceptions(method);
		for (final Class<?> throwableException: exceptions) {
			if (throwableException.isInstance(ex)) {
				this.throwException(ex);
			}
		}
		throw new InvocationException(ex);

	}

	private void throwException(final Throwable ex) throws Throwable {
		final StackTraceElement[] serverStackTrace = ex.getStackTrace();
		final StackTraceElement[] networkStackTrace = new StackTraceElement[] {new StackTraceElement("===Network", "Network", null, -1)};
		final StackTraceElement[] clientStackTrace = Thread.currentThread().getStackTrace();
		final int combinedStackTraceSize = serverStackTrace.length + networkStackTrace.length + clientStackTrace.length;
		final StackTraceElement[] combinedStackTrace = new StackTraceElement[combinedStackTraceSize];

		int offset = 0;
		System.arraycopy(serverStackTrace, 0, combinedStackTrace, offset, serverStackTrace.length);
		offset = serverStackTrace.length;
		System.arraycopy(networkStackTrace, 0, combinedStackTrace, offset, networkStackTrace.length);
		offset += networkStackTrace.length;
		System.arraycopy(clientStackTrace, 0, combinedStackTrace, offset, clientStackTrace.length);

		ex.setStackTrace(combinedStackTrace);
		throw ex;
	}

	private Set<Class<?>> resolveThrowableExceptions(final Method method) {
		final Set<Class<?>> exceptions = Collections.newSetFromMap(new IdentityHashMap<Class<?>, Boolean>());
		Class<?> clazz = method.getDeclaringClass();
		while (clazz != Object.class && clazz != null) {
			try {
				final Method method2 = clazz.getMethod(method.getName(), method.getParameterTypes());
				exceptions.addAll(Arrays.asList(method2.getExceptionTypes()));
			} catch (final NoSuchMethodException e) {
				break;
			}
			clazz = clazz.getSuperclass();
		}
		return exceptions;
	}

	private MarshalledInvocation createMarshalledInvocation(final InvocationContext invocationContext) {
		final MarshalledInvocation invocation = new MarshalledInvocation();
		invocation.setArguments(invocationContext.getArgs());
		invocation.setInterfaceName(invocationContext.getRemoteInterface().getName());
		invocation.setMethodName(invocationContext.getMethod().getName());

		final Class<?>[] types = invocationContext.getMethod().getParameterTypes();
		final String[] typeNames = new String[types.length];
		for (int i = 0; i < typeNames.length; i++) {
			typeNames[i] = types[i].getName();
		}
		invocation.setParameterTypeNames(typeNames);
		return invocation;
	}
}
